﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Famofo.AgbaraVOIP.Core.FreeSwitch;
using Famofo.AgbaraVOIP.Rest.Util;

namespace Famofo.AgbaraVOIP.Rest.FreeSwitch
{
    public class RESTInboundSocket : InboundSocket
    {
        const string EVENT_FILTER = "BACKGROUND_JOB CHANNEL_PROGRESS CHANNEL_PROGRESS_MEDIA CHANNEL_HANGUP CHANNEL_STATE";
        private string fs_outbound_address;
        private string auth_id;
        private string auth_token;
        private string default_http_method;
        public Dictionary<string, string> bk_jobs;
        public Dictionary<string, string> xfer_jobs;
        public Dictionary<string, CallRequest> call_requests;
        public RESTInboundSocket(string host, int port, string password, string outbound_address = "", string auth_id = "", string auth_token = "", string default_http_method = "POST")
            : base(host, port, password, EVENT_FILTER, 5, false)
        {
            fs_outbound_address = outbound_address;
            this.auth_id = auth_id;
            this.auth_token = auth_token;
            //# Mapping of Key: job-uuid - Value: request_uuid
            bk_jobs = new Dictionary<string, string>();
            //# Transfer jobs: call_uuid - Value: inline dptools to execute
            xfer_jobs = new Dictionary<string, string>();
            //# Call Requests
            call_requests = new Dictionary<string, CallRequest>();
            this.default_http_method = default_http_method;
            base.OnBACKGROUND_JOB +=new EventHandlers(ON_BACKGROUND_JOB);
        }

        //public override void LoadHandlers()
        //{
        //    event_handlers.Add("BACKGROUND_JOB", ON_BACKGROUND_JOB);
        //    event_handlers.Add("CHANNEL_PROGRESS", ON_CHANNEL_PROGRESS);
        //    event_handlers.Add("CHANNEL_PROGRESS_MEDIA", ON_CHANNEL_PROGRESS_MEDIA);
        //    event_handlers.Add("CHANNEL_HANGUP", ON_CHANNEL_HANGUP);
        //    event_handlers.Add("CHANNEL_STATE", ON_CHANNEL_STATE);
            
        //}
        
        public void ON_BACKGROUND_JOB(Event ev)
        {
            CallRequest call_req = new CallRequest(null, null, null, null, null);

            string status = string.Empty;
            string reason = string.Empty;
            //Capture Job Event
            // Capture background job only for originate and ignore all other jobs
            string job_cmd = ev.get_header("Job-Command");
            string job_uuid = ev.get_header("Job-UUID");
            if (job_cmd == "originate" && string.IsNullOrEmpty(job_uuid))
            {
                try
                {
                    //               status, reason = ev.get_body().Split(new Char[] = {' ')
                    string data = ev.get_body();
                    int pos = data.IndexOf(' ');
                    if (pos != -1)
                    {
                        status = data.Substring(0, pos).Trim();
                        reason = data.Substring(pos + 1).Trim();
                    }
                }
                catch (Exception ex)
                {
                    //except ValueError:
                    return;
                }
                string request_uuid = bk_jobs[job_uuid];
                if (string.IsNullOrEmpty(request_uuid))
                {
                    return;
                }
                try
                {
                    call_req = call_requests[request_uuid];
                }
                catch (Exception ex)
                {
                    //except KeyError:
                    return;
                }
                //# Handle failure case of originate
                //# This case does not raise a on_channel_hangup event.
                //# All other failures will be captured by on_channel_hangup
                if (!status.Contains("OK"))
                    //# In case ring/early state done, just warn
                    //# releasing call request will be done in hangup event
                    if (call_req.state_flag == "Ringing" || call_req.state_flag == "EarlyMedia")
                    {
                        //log.error("Call Attempt Done (%s) for RequestUUID %s but Failed (%s)" % (call_req.state_flag, request_uuid, reason))
                        return;
                    }
                    //# If no more gateways, release call request
                    else if (call_req.gateways.Count == 0)
                    {
                        //log.error("Call Failed for RequestUUID %s but No More Gateways (%s)"  % (request_uuid, reason))
                        //# set an empty call_uuid
                        string call_uuid = "";
                        string hangup_url = call_req.hangup_url;
                        set_hangup_complete(request_uuid, call_uuid, reason, ev, hangup_url);
                        return;
                    }
                    //If there are gateways and call request state_flag is not set
                    //try again a call
                    else if (call_req.gateways.Count > 0)
                        //log.warn("Call Failed without Ringing/EarlyMedia for RequestUUID %s - Retrying Now (%s)" ,(request_uuid, reason))   spawn_originate(request_uuid)
                        spawn_originate(request_uuid);
                
            }
        }
        public void ON_CHANNEL_PROGRESS(Event ev)
        {
            CallRequest call_req;
            string request_uuid = ev.get_header("variable_agbaravoip_request_uuid");
            string direction = ev.get_header("Call-Direction");
            // Detect ringing state
            if (!string.IsNullOrEmpty(request_uuid) && direction == "outbound")
            {
                try
                {
                    call_req = call_requests[request_uuid];
                }
                catch (Exception ex)
                {
                    return;
                }
                //only send if not already ringing/early state
                if (!string.IsNullOrEmpty(call_req.state_flag))
                {
                    // set state flag to true
                    call_req.state_flag = "Ringing";
                    //clear gateways to avoid retry
                    call_req.gateways.Clear();
                    string called_num = ev.get_header("Caller-Destination-Number");
                    string caller_num = ev.get_header("Caller-Caller-ID-Number");
                    ///self.log.info("Call from %s to %s Ringing for RequestUUID %s" (caller_num, called_num, request_uuid))
                    // send ring if ring_url found
                    string ring_url = call_req.ring_url;
                    if (!string.IsNullOrEmpty(ring_url))
                    {
                        Hashtable param = new Hashtable();
                        param.Add("RequestUUID", request_uuid);
                        param.Add("Direction", direction);
                        param.Add("To", called_num);
                        param.Add("CallStatus", "ringing");
                        param.Add("From", caller_num);
                        string response = Task<string>.Factory.StartNew(() => send_to_url(ring_url, param, string.Empty)).Result;
                    }
                }
            }

        }
        public void ON_CHANNEL_PROGRESS_MEDIA(Event ev)
        {
            CallRequest call_req;
            string request_uuid = ev.get_header("variable_plivo_request_uuid");
            string direction = ev.get_header("Call-Direction");
            //# Detect early media state
            // See http://wiki.freeswitch.org/wiki/Early_media#Early_Media_And_Dialing_Out
            if (!string.IsNullOrEmpty(request_uuid) && direction == "outbound")
            {
                try
                {
                    call_req = call_requests[request_uuid];
                }
                catch (Exception ex)
                {
                    return;
                }
                // only send if not already ringing/early state
                if (!string.IsNullOrEmpty(call_req.state_flag))
                    //# set state flag to true
                    call_req.state_flag = "EarlyMedia";
                // clear gateways to avoid retry
                call_req.gateways.Clear();
                string called_num = ev.get_header("Caller-Destination-Number");
                string caller_num = ev.get_header("Caller-Caller-ID-Number");
                //log.info("Call from %s to %s in EarlyMedia for RequestUUID %s"% (caller_num, called_num, request_uuid))
                // send ring if ring_url found
                string ring_url = call_req.ring_url;
                if (!string.IsNullOrEmpty(ring_url))
                {
                    Hashtable param = new Hashtable();
                    param.Add("RequestUUID", request_uuid);
                    param.Add("Direction", direction);
                    param.Add("To", called_num);
                    param.Add("CallStatus", "ringing");
                    param.Add("From", caller_num);
                    string response = Task<string>.Factory.StartNew(() => send_to_url(ring_url, param, string.Empty)).Result;
                }
            }
        }
        public void ON_CHANNEL_HANGUP(Event ev)
        {
            CallRequest call_req;
            string request_uuid = ev.get_header("variable_plivo_request_uuid");
            string direction = ev.get_header("Call-Direction");
            if (!string.IsNullOrEmpty(request_uuid) && direction != "outbound")
                return;
            string call_uuid = ev.get_header("Unique-ID");
            string reason = ev.get_header("Hangup-Cause");
            try
            {
                call_req = call_requests[request_uuid];
            }
            catch (Exception ex)
            {
                return;
            }
            // If there are gateways to try again, spawn originate
            if (call_req.gateways.Count > 0)
            {
                //self.log.debug("Call Failed for RequestUUID %s - Retrying (%s)" % (request_uuid, reason))
                spawn_originate(request_uuid);
                return;
            }
            else // Else clean call request
            {
                string hangup_url = call_req.hangup_url;
                set_hangup_complete(request_uuid, call_uuid, reason, ev, hangup_url);
            }

        }
        public void ON_CHANNEL_STATE(Event ev)
        {
            // When tranfer is ready to start,
            // channel goes in state CS_RESET


            if (ev.get_header("Channel-State") == "CS_RESET")
            {
                string call_uuid = ev.get_header("Unique-ID");
                string xfer = xfer_jobs[call_uuid];
                if (string.IsNullOrEmpty(xfer))
                    return;
                //log.info("TransferCall In Progress for %s" % call_uuid)
                //# unset transfer progress flag
                set_var("agbaravoip_transfer_progress", "false", call_uuid);
                //really transfer now
                APIResponse res = (APIResponse)api(string.Format("uuid_transfer {0} {1} inline", call_uuid, xfer));
                // if (res.IsSuccess())
                //log.info("TransferCall Done for %s" % call_uuid)
                //else
                //log.info("TransferCall Failed for %s: %s"     % (call_uuid, res.get_response()))
            }
            //# On state CS_HANGUP, remove transfer job linked to call_uuid
            else if (ev.get_header("Channel-State") == "CS_HANGUP")
            {
                string call_uuid = ev.get_header("Unique-ID");
                //# try to clean transfer call
                string xfer = xfer_jobs[call_uuid];
                if (!string.IsNullOrEmpty(xfer))
                {
                    //log.warn("TransferCall Aborted (hangup) for %s" % call_uuid)
                }
            }
        }
        private void set_hangup_complete(string request_uuid, string call_uuid, string reason, Event ev, string hangup_url)
        {
            //self.log.info("Call %s Completed, Reason %s, Request uuid %s" % (call_uuid, reason, request_uuid))
            try
            {
                //call_requests[request_uuid] = None
                call_requests.Remove(request_uuid);
            }
            catch (Exception ex)
            {
                //except KeyError, AttributeError:
                //pass
            }
            //self.log.debug("Call Cleaned up for RequestUUID %s" % request_uuid)
            if (!string.IsNullOrEmpty(hangup_url))
            {
                string called_num = ev.get_header("Caller-Destination-Number");
                string caller_num = ev.get_header("Caller-Caller-ID-Number");

                Hashtable param = new Hashtable();
                param.Add("RequestUUID", request_uuid);
                param.Add("CallUUID", call_uuid);
                param.Add("HangupCause", reason);
                param.Add("Direction", "outbound");
                param.Add("To", called_num);
                param.Add("CallStatus", "completed");
                param.Add("From", caller_num);

                string response = Task<string>.Factory.StartNew(() => send_to_url(hangup_url, param, string.Empty)).Result;
                //log response
            }
            else
            {
                //self.log.debug("No hangupUrl for RequestUUID %s" % request_uuid);
            }
        }
        private string send_to_url(string url, Hashtable param, string method)
        {
            string data = string.Empty;
            if (string.IsNullOrEmpty(method))
                method = default_http_method;

            if (string.IsNullOrEmpty(url))
            {
                //self.log.warn("Cannot send %s, no url !" % method)
                return string.Empty;
            }
            HTTPRequest http_obj = new HTTPRequest(auth_id, auth_token);
            try
            {
                data = http_obj.fetch_response(url, method, param);
                //self.log.info("Sent to %s %s with %s -- Result: %s"% (method, url, param, data))
            }
            catch (Exception ex)
            {
                //self.log.error("Sending to %s %s with %s -- Error: %s" % (method, url, params, e))
            }
            return data;
        }
        public bool spawn_originate(string request_uuid)
        {
            CallRequest call_req;
            bool ret = false;
            Gateway gw = new Gateway(null, null, null, null, null, null);
            try
            {
                call_req = call_requests[request_uuid];
            }
            catch (Exception ex)
            {
                //log.warn("Call Request not found for RequestUUID %s" % request_uuid)
                return false;
            }

            try
            {
                gw = call_req.gateways.Pop();
            }
            catch (Exception ex)
            {

                //log.warn("No more Gateways to call for RequestUUID %s" % request_uuid)
            }
            try
            {
                call_requests.Remove(request_uuid);
            }
            catch (Exception ex)
            {
                //return;
            }

            //List<string> _options = new List<string>();//[]
            StringBuilder _options = new StringBuilder();
            //# Set agbaravoip app flag
            _options.Append("agbaravoip_app=true");
            if (!string.IsNullOrEmpty(gw.codecs))
            {
                _options.Append(string.Format("absolute_codec_string={0}", gw.codecs));
            }
            if (!string.IsNullOrEmpty(gw.timeout))
            {
                _options.Append(string.Format("originate_timeout={0}", gw.timeout));
            }
            _options.Append("ignore_early_media=true");

            string options = string.Join(",", _options);
            string outbound_str = string.Format("'socket:{0} async full' inline", fs_outbound_address);

            string dial_str = string.Format("originate {{0},{1}}{2}/{3} {4}", gw.extra_dial_string, options, gw.gw, gw.to, outbound_str);

            BgApiResponse bg_api_response = (BgApiResponse)Task<Event>.Factory.StartNew(() => bgapi(dial_str)).Result;
            
            
            if (bg_api_response.IsSuccess())
            {
                string job_uuid = bg_api_response.GetJobUUID();
                if (!string.IsNullOrEmpty(job_uuid))
                {
                    
                    bk_jobs[job_uuid] = request_uuid;
                    ret = true;
                }
                else
                {
                    ret = false;
                    //log.error("Call Failed for RequestUUID {0} -- JobUUID not received", request_uuid);
                }
            }
            return ret;
        }
        private bool bulk_originate(List<string> request_uuid_list)
        {
            bool ret;

            if (request_uuid_list.Count > 0)
            {
                //log.info("BulkCall for RequestUUIDs %s" % str(request_uuid_list))
                foreach (string reqId in request_uuid_list)
                {
                    string request_uuid = reqId;
                    Task.Factory.StartNew(() => spawn_originate(request_uuid));
                }
                ret = true;
            }
            else
            {
                //log.error("BulkCall Failed -- No RequestUUID !")
                ret = false;
            }
            return ret;
        }
        internal bool transfer_call(string new_xml_url, string call_uuid)
        {
            bool ret;
            //# Set transfer progress flag to prevent hangup 
            // # when the current outbound_socket flow will end
            set_var("agbaravoip_transfer_progress", "true", call_uuid);
            //# Set transfer url
            set_var("agbaravoip_transfer_url", new_xml_url, call_uuid);
            //# Link inline dptools (will be run when ready to start transfer) 
            //# to the call_uuid job
            string outbound_str = string.Format("socket:%s async full", fs_outbound_address);
            xfer_jobs[call_uuid] = outbound_str;
            // # Transfer into sleep state a little waiting for real transfer
            APIResponse res = (APIResponse)api(string.Format("uuid_transfer {0} sleep:5000 inline", call_uuid));
            if (res.IsSuccess())
            {
                //log.info("TransferCall Spawned for %s" % call_uuid)
                ret = true;
                //# On failure, remove the job and log error
                try
                {
                    xfer_jobs.Remove(call_uuid);
                }
                catch (Exception ex)
                {
                    //log error
                }
            }
            else
            {

                //error("TransferCall Spawning Failed for %s : %s" \% (call_uuid, str(res.get_response())))
                ret = false;
            }
            return ret;
        }
        internal bool hangup_call(string call_uuid = "", string request_uuid = "")
        {
            bool ret;
            string callid;
            string cmd;
            if (string.IsNullOrEmpty(call_uuid) && string.IsNullOrEmpty(request_uuid))
            {
                //log.error("Call Hangup Failed -- Missing CallUUID or RequestUUID")
                ret = false;
            }

            if (!string.IsNullOrEmpty(call_uuid))
            {
                callid = string.Format("CallUUID {0}", call_uuid);
                cmd = string.Format("uuid_kill{0} NORMAL_CLEARING", call_uuid);
            }
            else  //# Use request uuid
            {
                callid = string.Format("RequestUUID {0}", request_uuid);
                try
                {
                    CallRequest call_req = call_requests[request_uuid];
                }
                catch (Exception ex)
                {
                    //except (KeyError, AttributeError):
                    //self.log.error("Call Hangup Failed -- %s not found"   % (callid))
                    ret = false;
                    return ret;
                }
                //string callid = string.Format("RequestUUID {0}", request_uuid);
                cmd = string.Format("hupall NORMAL_CLEARING agbaravoip_request_uuid {0}", request_uuid);
            }
            APIResponse res = (APIResponse)api(cmd);
            if (!res.IsSuccess())
            {
                //self.log.error("Call Hangup Failed for %s -- %s"   % (callid, res.get_response()))
                return false;
            }
            //self.log.info("Executed Call Hangup for %s" % callid)
            return true;
        }
        internal bool hangup_all_calls()
        {
            bool ret = false;
            BgApiResponse bg_api_response = (BgApiResponse)bgapi("hupall NORMAL_CLEARING");
            string job_uuid = bg_api_response.GetJobUUID();
            if (string.IsNullOrEmpty(job_uuid))
            {
                //log.error("Hangup All Calls Failed -- JobUUID not received")
                ret = false;
            }
            else
            {
                ret = true;
            }
            //log.info("Executed Hangup for all calls")
            return ret;
        }
    }
}
